/**
 * @Date: Mar 2, 2010 7:40:24 PM
 */
package com.philip.journal.core.controller;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.philip.core.WarningType;
import com.philip.journal.common.BeanUtils;
import com.philip.journal.common.StringUtils;
import com.philip.journal.core.controller.spring.BaseServletSpring;
import com.philip.journal.core.exception.JournalException;
import com.philip.journal.core.service.ServiceProxy;
import com.philip.journal.login.service.exception.IncorrectPasswordException;

/**
 * This class is the parent of all controller servlets in the application.
 *
 * NOTE: To migrate out of spring, simple populate the controller map via the init method.
 *
 * In the event that the application is migrated out of Spring, this class must be modified only to extend the
 * HttpServlet instead of the current BaseServletSpring.
 *
 * Synchronization is needed with this approach of hiding the request and response from the action classes.
 * Action classes aren't really prototyped (huh?!), intentionally non-singleton but this is not possible with
 * spring in this design. Passing the request and response as method parameters for the action classes execute
 * is the better approach to handle thread safety.
 *
 * @author cry30
 */
public abstract class BaseServlet extends BaseServletSpring { // NOPMD by r39 on 3/30/11 11:17 AM

    /** Constant for POST request. */
    public static final String METHOD_POST = "POST";

    /** Constant for GET request. */
    public static final String METHOD_GET = "GET";

    /** Primary page action/event identified. */
    public static final String ACTION = "action";

    /** Sub class reusable logger instance. */
    private final Logger logger = Logger.getLogger(getClass()); // NOPMD by r39

    /**
     * Reference to Service proxy for servlets that do not utilize action classes. Action class has their
     * service proxy automatically injected to them directly.
     */
    private transient ServiceProxy serviceProxy;

    /** Reference to action mapping. */
    private transient Map<String, Object> actionMap;

    /** Reference to default page to forward to. */
    private transient String defaultGetPage;

    /** Will ignore this character if passed in the parameters. */
    private static final int ZERO_WIDTH_STR = 8203;

    @Override
    public void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException,
            IOException {
        getLogger().info("service()..." + request.getMethod() + " : " + request.getParameter(ACTION));

        if (getLogger().isDebugEnabled()) {
            printRequestParameters(request);
        }

        final String action = request.getParameter(ACTION);
        final Object actionObject = actionMap.get(action == null ? "" : action);
        ControllerAction controllerAction = null;

        synchronized (this) {
            if (actionObject instanceof ControllerAction) {
                controllerAction = (ControllerAction) actionObject;
            }
            final boolean hasCtrlAndAction = controllerAction != null || actionObject != null;
            if (hasCtrlAndAction) {
                serviceHasActionAndControl(request, response);
            } else if (request.getMethod().equalsIgnoreCase(METHOD_GET)) {
                if (action != null && !"".equals(action)) {
                    logger.warn("ActionProcessor for [" + action + "] is not found in ControllerServlet.");
                }
                request.getRequestDispatcher(defaultGetPage).include(request, response);
            } else {
                Logger.getLogger(BaseServletSpring.class).fatal(
                        request.getServletPath() + "?action=" + action + " is not found in ControllerServlet.");
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        }
    }

/**
     * Refactored out for simplification. @see
     * {@com.philip.journal.core.controller.BaseServlet.service(HttpServletRequest, HttpServletResponse)
     *
     * @param request - standard servlet request.
     * @param response - standard servlet response.
     * @throws ServletException - standard servlet exception adapted by the spring interface.
     * @throws IOException - standard servlet exception adapted by the spring interface.
     */
    private void serviceHasActionAndControl(final HttpServletRequest request, final HttpServletResponse response)
            throws ServletException, IOException {
        final String action = request.getParameter(ACTION);
        final Object actionObject = actionMap.get(action == null ? "" : action);
        ControllerAction controllerAction = null;
        if (actionObject instanceof ControllerAction) {
            controllerAction = (ControllerAction) actionObject;
        }

        BeanUtils.invokeMethodSilent(actionObject, "setSessionId", new Class[] { String.class }, new Object[] { request
                .getSession().getId() });

        setActionProperties(request, actionObject);
        BeanUtils.invokeMethodSilent(actionObject, "setHttpServletRequest", new Class[] { HttpServletRequest.class },
                new Object[] { request });
        BeanUtils.invokeMethodSilent(actionObject, "setHttpServletResponse", new Class[] { HttpServletResponse.class },
                new Object[] { response });
        try {
            BeanUtils.invokeMethod(actionObject, "execute", new Class[0], new Object[0]);
        } catch (final InvocationTargetException ite) {
            getLogger().debug(ite.getMessage(), ite);

            if (ite.getCause() instanceof IncorrectPasswordException) {
                controllerAction.getResponseHandler().respondFail(response, ite.getMessage());
            } else if (ite.getCause() instanceof JournalException) {
                final Throwable appException = ite.getCause();

                final String detailMsg = appException.getMessage();

                final boolean hasStackTrace = detailMsg.indexOf(":") > 0;
                if (hasStackTrace) {
                    final String shortDesc = detailMsg.substring(0, detailMsg.indexOf(":"));
                    final StringBuilder longDesc = new StringBuilder(detailMsg.substring(appException.getMessage()
                            .indexOf(":") + 1));
                    longDesc.append("\n" + getStringUtils().stackTraceToString(appException));
                    controllerAction.getResponseHandler().respondFail(response, shortDesc, longDesc.toString());
                } else {
                    controllerAction.getResponseHandler().respondFail(response, appException.getCause().toString(),
                            detailMsg);
                }
            }
        } catch (final Exception e) {
            logger.error(e.getMessage(), e);
            response.sendError(HttpServletResponse.SC_ACCEPTED);
        }
        Logger.getLogger(BaseServletSpring.class).debug("Action [" + action + "] finished");

    }

    /**
     * Attempts to set the action bean properties using the form parameters.
     *
     * @param request - the current HttpServletRequest.
     * @param actionObject - action object.
     */
    protected final void setActionProperties(final HttpServletRequest request, final Object actionObject) {
        Class<?> retType = null;
        final List<String> skippedParams = new ArrayList<String>();
        final Map<String, Object> setParams = new HashMap<String, Object>();

        final Object[] objArrParam = new Object[1];
        final Class<?>[] clsArrParam = new Class[1];
        for (@SuppressWarnings(WarningType.UNCHECKED)
        final Enumeration<String> en = request.getParameterNames(); en.hasMoreElements();) {
            final String propertyName = en.nextElement();
            try {
                final String setterMethod = BeanUtils.getSetterMethodName(propertyName);
                retType = BeanUtils.getPropertyType(actionObject, propertyName);
                clsArrParam[0] = retType;
                final Method method = actionObject.getClass().getMethod(setterMethod, clsArrParam);
                if (method != null) {
                    Object value = request.getParameter(propertyName);
                    if (retType.equals(Integer.TYPE)) {
                        value = Integer.parseInt(value.toString());
                    } else if (retType.equals(Long.TYPE)) {
                        value = Long.parseLong(value.toString());
                    } else if (retType.equals(String.class)) {
                        value = value.toString().replace(Character.toString((char) ZERO_WIDTH_STR), "");
                    }

                    objArrParam[0] = value;
                    method.invoke(actionObject, objArrParam);
                    setParams.put(propertyName, getStringUtils().removeHtmlTags(value.toString()));
                }
            } catch (final Exception ignore) {
                skippedParams.add(propertyName);
            }
        }
        getLogger().debug("Set params: " + setParams);
        getLogger().debug("Skipped params: " + actionObject.getClass().getSimpleName() + " : " + skippedParams);
    }

    /**
     * Tracing method.
     *
     * @param pRequest HttpServletRequest.
     */
    @SuppressWarnings("unchecked")
    private void printRequestParameters(final HttpServletRequest pRequest) {
        final Map<String, String> map = new HashMap<String, String>();
        for (final Enumeration<String> en = pRequest.getParameterNames(); en.hasMoreElements();) {
            final String paramName = en.nextElement();
            map.put(paramName, getStringUtils().removeHtmlTags(pRequest.getParameter(paramName)));
        }
        logger.debug("Parameters: " + map);
    }

    StringUtils getStringUtils() {
        return StringUtils.getInstance();
    }

    /**
     * Setter for action mapping.
     *
     * @param pActionMap action mapping instance.
     */
    public final void setActionMap(final Map<String, Object> pActionMap) {
        this.actionMap = pActionMap;
    }

    /**
     * Accessor method.
     *
     * @param pDefaultGetPage default page to forward for sub classing servlet.
     */
    public final void setDefaultGetPage(final String pDefaultGetPage) {
        this.defaultGetPage = pDefaultGetPage;
    }

    /**
     * Accessor method.
     *
     * @param pServiceProxy ServiceProxy to set.
     */
    public final void setServiceProxy(final ServiceProxy pServiceProxy) {
        this.serviceProxy = pServiceProxy;
    }

    /**
     * @return ServiceProxy for use with servlet sub class.
     */
    public final ServiceProxy getServiceProxy() {
        return this.serviceProxy;
    }

    /**
     * Accessor method.
     *
     * @return the logger
     */
    public final Logger getLogger() {
        return logger;
    }
}
